/*******************************************************************************
 * Copyright (c) 2007, 2015 Ericsson and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Ericsson - Initial API and implementation
 *******************************************************************************/

package org.eclipse.cdt.examples.dsf.pda.service;

import java.util.HashSet;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;

import org.eclipse.cdt.dsf.concurrent.DataRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.Immutable;
import org.eclipse.cdt.dsf.concurrent.RequestMonitor;
import org.eclipse.cdt.dsf.datamodel.AbstractDMContext;
import org.eclipse.cdt.dsf.datamodel.DMContexts;
import org.eclipse.cdt.dsf.datamodel.IDMContext;
import org.eclipse.cdt.dsf.debug.service.IBreakpoints;
import org.eclipse.cdt.dsf.service.AbstractDsfService;
import org.eclipse.cdt.dsf.service.DsfSession;
import org.eclipse.cdt.examples.dsf.pda.PDAPlugin;
import org.eclipse.cdt.examples.dsf.pda.breakpoints.PDAWatchpoint;
import org.eclipse.cdt.examples.dsf.pda.service.commands.PDAClearBreakpointCommand;
import org.eclipse.cdt.examples.dsf.pda.service.commands.PDACommandResult;
import org.eclipse.cdt.examples.dsf.pda.service.commands.PDASetBreakpointCommand;
import org.eclipse.cdt.examples.dsf.pda.service.commands.PDAWatchCommand;
import org.eclipse.core.resources.IMarker;
import org.eclipse.debug.core.model.IBreakpoint;
import org.osgi.framework.BundleContext;

/**
 * Initial breakpoint service implementation.
 * Implements the IBreakpoints interface.
 */
public class PDABreakpoints extends AbstractDsfService implements IBreakpoints
{
    /**
     * Context representing a PDA line breakpoint.  In PDA debugger, since there is only 
     * one file being debugged at a time, a breakpoint is uniquely identified using the 
     * line number only.
     */
    @Immutable
    private static class BreakpointDMContext extends AbstractDMContext implements IBreakpointDMContext {

        final Integer fLine;

        public BreakpointDMContext(String sessionId, PDAVirtualMachineDMContext commandControlCtx, Integer line) {
            super(sessionId, new IDMContext[] { commandControlCtx });
            fLine = line;
        }

        @Override
        public boolean equals(Object obj) {
            return baseEquals(obj) && (fLine.equals(((BreakpointDMContext) obj).fLine));
        }

        @Override
        public int hashCode() {
            return baseHashCode() + fLine.hashCode();
        }

        @Override
        public String toString() {
            return baseToString() + ".breakpoint(" + fLine + ")";  //$NON-NLS-1$//$NON-NLS-2$*/
        }
    }

    /**
     * Context representing a watch point.  In PDA debugger, a watchpoint is 
     * uniquely identified using the function and variable.
     */
    @Immutable
    private static class WatchpointDMContext extends AbstractDMContext implements IBreakpointDMContext {
        final String fFunction;
        final String fVariable; 

        public WatchpointDMContext(String sessionId, PDAVirtualMachineDMContext commandControlCtx, String function, 
            String variable) 
        {
            super(sessionId, new IDMContext[] { commandControlCtx });
            fFunction = function;
            fVariable = variable;
        }

        @Override
        public boolean equals(Object obj) {
            if (baseEquals(obj)) {
                WatchpointDMContext watchpointCtx = (WatchpointDMContext)obj;
                return fFunction.equals(watchpointCtx.fFunction) && fVariable.equals(watchpointCtx.fVariable);
            }
            return false;
        }

        @Override
        public int hashCode() {
            return baseHashCode() + fFunction.hashCode() + fVariable.hashCode();
        }

        @Override
        public String toString() {
            return baseToString() + ".watchpoint(" + fFunction + "::" + fVariable + ")";
        }
    }

    // Attribute names
    public static final String ATTR_BREAKPOINT_TYPE = PDAPlugin.PLUGIN_ID + ".pdaBreakpointType";      //$NON-NLS-1$
    public static final String PDA_LINE_BREAKPOINT = "breakpoint";                 //$NON-NLS-1$
    public static final String PDA_WATCHPOINT = "watchpoint";                 //$NON-NLS-1$
    public static final String ATTR_PROGRAM_PATH = PDAPlugin.PLUGIN_ID + ".pdaProgramPath";      //$NON-NLS-1$

    // Services
    private PDACommandControl fCommandControl;

    // Breakpoints currently installed
    private Set<IBreakpointDMContext> fBreakpoints = new HashSet<IBreakpointDMContext>();

    /**
     * The service constructor
     * 
     * @param session The debugging session this service belongs to.
     */
    public PDABreakpoints(DsfSession session) {
        super(session);
    }

    @Override
    public void initialize(final RequestMonitor rm) {
        super.initialize(new RequestMonitor(getExecutor(), rm) {
            @Override
            protected void handleSuccess() {
                doInitialize(rm);
            }
        });
    }

    private void doInitialize(final RequestMonitor rm) {
        // Get the services references
        fCommandControl = getServicesTracker().getService(PDACommandControl.class);

        // Register this service
        register(new String[] { IBreakpoints.class.getName(), PDABreakpoints.class.getName() },
            new Hashtable<String, String>());

        rm.done();
    }

    @Override
    public void shutdown(final RequestMonitor rm) {
        unregister();
        rm.done();
    }

    @Override
    protected BundleContext getBundleContext() {
        return PDAPlugin.getBundleContext();
    }

    @Override
    public void getBreakpoints(final IBreakpointsTargetDMContext context, final DataRequestMonitor<IBreakpointDMContext[]> rm) {
        // Validate the context
        if (!fCommandControl.getContext().equals(context)) {
            PDAPlugin.failRequest(rm, INVALID_HANDLE, "Invalid breakpoints target context");
            return;
        }

        rm.setData(fBreakpoints.toArray(new IBreakpointDMContext[fBreakpoints.size()]));
        rm.done();
    }

    @Override
    public void getBreakpointDMData(IBreakpointDMContext dmc, DataRequestMonitor<IBreakpointDMData> rm) {
        PDAPlugin.failRequest(rm, NOT_SUPPORTED, "Retrieving breakpoint data is not supported");
    }

    @Override
    public void insertBreakpoint(IBreakpointsTargetDMContext context, Map<String, Object> attributes, 
        DataRequestMonitor<IBreakpointDMContext> rm) 
    {
        Boolean enabled = (Boolean)attributes.get(IBreakpoint.ENABLED);
        if (enabled != null && !enabled.booleanValue()) {
            // If the breakpoint is disabled, just fail the request. 
            PDAPlugin.failRequest(rm, REQUEST_FAILED, "Breakpoint is disabled");
        } else {
            String type = (String) attributes.get(ATTR_BREAKPOINT_TYPE);

            if (PDA_LINE_BREAKPOINT.equals(type)) {
                // Retrieve the PDA program context from the context given in the 
                // argument.  This service is typically only called by the 
                // breakpoints mediator, which was called with the program context
                // in the services initialization sequence.  So checking if 
                // programCtx != null is mostly a formality.
                PDAVirtualMachineDMContext programCtx = DMContexts.getAncestorOfType(context, PDAVirtualMachineDMContext.class);
                if (programCtx != null) {
                    doInsertBreakpoint(programCtx, attributes, rm);
                } else {
                    PDAPlugin.failRequest(rm, INVALID_HANDLE, "Unknown breakpoint type");
                }
            }
            else if (PDA_WATCHPOINT.equals(type)) {
                doInsertWatchpoint(attributes, rm);
            }
            else {
                PDAPlugin.failRequest(rm, REQUEST_FAILED, "Unknown breakpoint type");
            }
        }
    }

    private void doInsertBreakpoint(PDAVirtualMachineDMContext programCtx, final Map<String, Object> attributes, final DataRequestMonitor<IBreakpointDMContext> rm) 
    {
        // Compare the program path in the breakpoint with the path in the PDA 
        // program context. Only insert the breakpoint if the program matches. 
        String program = (String)attributes.get(ATTR_PROGRAM_PATH);
        if (!programCtx.getProgram().equals(program)) {
            PDAPlugin.failRequest(rm, REQUEST_FAILED, "Invalid file name");
            return;
        }

        // Retrieve the line.
        Integer line = (Integer)attributes.get(IMarker.LINE_NUMBER);
        if (line == null) {
            PDAPlugin.failRequest(rm, REQUEST_FAILED, "No breakpoint line specified");
            return;
        }

        // Create a new breakpoint context object and check that it's not 
        // installed already. PDA can only track a single breakpoint at a 
        // given line, attempting to set the second breakpoint should fail.
        final BreakpointDMContext breakpointCtx = 
            new BreakpointDMContext(getSession().getId(), fCommandControl.getContext(), line);
        if (fBreakpoints.contains(breakpointCtx)) {
            PDAPlugin.failRequest(rm, REQUEST_FAILED, "Breakpoint already set");
            return;
        }

        // Add the new breakpoint context to the list of known breakpoints.  
        // Adding it here, before the set command is completed will prevent 
        // a possibility of a second breakpoint being installed in the same 
        // location while this breakpoint is being processed.  It will also
        // allow the breakpoint to be removed or updated even while it is 
        // still being processed here.
        fBreakpoints.add(breakpointCtx);
        fCommandControl.queueCommand(
            new PDASetBreakpointCommand(fCommandControl.getContext(), line, false), 
            new DataRequestMonitor<PDACommandResult>(getExecutor(), rm) {
                @Override
                protected void handleSuccess() {
                    rm.setData(breakpointCtx);
                    rm.done();
                }

                @Override
                protected void handleFailure() {
                    // If inserting of the breakpoint failed, remove it from
                    // the set of installed breakpoints.
                    fBreakpoints.remove(breakpointCtx);
                    super.handleFailure();
                }
            });
    }

    private void doInsertWatchpoint(final Map<String, Object> attributes, final DataRequestMonitor<IBreakpointDMContext> rm) 
    {
        String function = (String)attributes.get(PDAWatchpoint.FUNCTION_NAME);
        if (function == null) {
            PDAPlugin.failRequest(rm, REQUEST_FAILED, "No function specified");
            return;
        }

        String variable = (String)attributes.get(PDAWatchpoint.VAR_NAME);
        if (variable == null) {
            PDAPlugin.failRequest(rm, REQUEST_FAILED, "No variable specified");
            return;
        }

        Boolean isAccess = (Boolean)attributes.get(PDAWatchpoint.ACCESS);
        isAccess = isAccess != null ? isAccess : Boolean.FALSE; 

        Boolean isModification = (Boolean)attributes.get(PDAWatchpoint.MODIFICATION);
        isModification = isModification != null ? isModification : Boolean.FALSE; 

        // Create a new watchpoint context object and check that it's not 
        // installed already. PDA can only track a single watchpoint for a given
        // function::variable, attempting to set the second breakpoint should fail.
        final WatchpointDMContext watchpointCtx = 
            new WatchpointDMContext(getSession().getId(), fCommandControl.getContext(), function, variable);
        if (fBreakpoints.contains(watchpointCtx)) {
            PDAPlugin.failRequest(rm, REQUEST_FAILED, "Watchpoint already set");
            return;
        }

        // Determine the watch operation to perform.
        PDAWatchCommand.WatchOperation watchOperation = PDAWatchCommand.WatchOperation.NONE;
        if (isAccess && isModification) {
            watchOperation = PDAWatchCommand.WatchOperation.BOTH;
        } else if (isAccess) {
            watchOperation = PDAWatchCommand.WatchOperation.READ;
        } else if (isModification) {
            watchOperation = PDAWatchCommand.WatchOperation.WRITE;
        }

        // Add the new breakpoint context to the list of known breakpoints.  
        // Adding it here, before the set command is completed will prevent 
        // a possibility of a second breakpoint being installed in the same 
        // location while this breakpoint is being processed.  It will also
        // allow the breakpoint to be removed or updated even while it is 
        // still being processed here.
        fBreakpoints.add(watchpointCtx);
        fCommandControl.queueCommand(
            new PDAWatchCommand(fCommandControl.getContext(), function, variable, watchOperation), 
            new DataRequestMonitor<PDACommandResult>(getExecutor(), rm) {
                @Override
                protected void handleSuccess() {
                    rm.setData(watchpointCtx);
                    rm.done();
                }

                @Override
                protected void handleFailure() {
                    // Since the command failed, we need to remove the breakpoint from 
                    // the existing breakpoint set.
                    fBreakpoints.remove(watchpointCtx);
                    super.handleFailure();
                }
            });
    }

    @Override
    public void removeBreakpoint(IBreakpointDMContext bpCtx, RequestMonitor rm) {
        if (!fBreakpoints.contains(bpCtx)) {
            PDAPlugin.failRequest(rm, REQUEST_FAILED, "Breakpoint already removed");
            return;
        }

        if (bpCtx instanceof BreakpointDMContext) {
            doRemoveBreakpoint((BreakpointDMContext)bpCtx, rm);
        } else if (bpCtx instanceof WatchpointDMContext) {
            doRemoveWatchpoint((WatchpointDMContext)bpCtx, rm);
        } else {
            PDAPlugin.failRequest(rm, INVALID_HANDLE, "Invalid breakpoint");
        }
    }

    private void doRemoveBreakpoint(BreakpointDMContext bpCtx, RequestMonitor rm) {
        // Remove the breakpoint from the table right away, so that even when 
        // the remove is being processed, a new breakpoint can be created at the same 
        // location.
        fBreakpoints.remove(bpCtx);

        fCommandControl.queueCommand(
            new PDAClearBreakpointCommand(fCommandControl.getContext(), bpCtx.fLine), 
            new DataRequestMonitor<PDACommandResult>(getExecutor(), rm));        
    }

    private void doRemoveWatchpoint(WatchpointDMContext bpCtx, RequestMonitor rm) {
        fBreakpoints.remove(bpCtx);

        // Watchpoints are cleared using the same command, but with a "no watch" operation
        fCommandControl.queueCommand(
            new PDAWatchCommand(
                fCommandControl.getContext(), bpCtx.fFunction, bpCtx.fVariable, PDAWatchCommand.WatchOperation.NONE), 
                new DataRequestMonitor<PDACommandResult>(getExecutor(), rm));        
    }

    @Override
    public void updateBreakpoint(final IBreakpointDMContext bpCtx, Map<String, Object> attributes, final RequestMonitor rm) {
        if (!fBreakpoints.contains(bpCtx)) {
            PDAPlugin.failRequest(rm, REQUEST_FAILED, "Breakpoint not installed");
            return;
        }

        if (bpCtx instanceof BreakpointDMContext) {
            PDAPlugin.failRequest(rm, NOT_SUPPORTED, "Modifying PDA breakpoints is not supported");
        } else if (bpCtx instanceof WatchpointDMContext) {
            WatchpointDMContext wpCtx = (WatchpointDMContext)bpCtx;
            if (!wpCtx.fFunction.equals(attributes.get(PDAWatchpoint.FUNCTION_NAME)) || 
                !wpCtx.fVariable.equals(attributes.get(PDAWatchpoint.VAR_NAME)) )
            {
                PDAPlugin.failRequest(rm, REQUEST_FAILED, "Cannot modify watchpoint function or variable");
                return;
            }
            
            // PDA debugger can only track one watchpoint in the same location, 
            // so we can simply remove the existing context from the set and 
            // call insert again.  
            fBreakpoints.remove(bpCtx);
            doInsertWatchpoint(
                attributes, 
                new DataRequestMonitor<IBreakpointDMContext>(getExecutor(), rm) {
                    @Override
                    protected void handleSuccess() {
                        // The inserted watchpoint context will equal the 
                        // current context.
                        assert bpCtx.equals(getData());
                        rm.done();
                    }
                });
        } else {
            PDAPlugin.failRequest(rm, INVALID_HANDLE, "Invalid breakpoint");
        }
    }
}
